نگاهی عمیق به مدیریت جریانهای داده در جاوااسکریپت. با مکانیسم فشار برگشتی ظریف ژنراتورهای ناهمگام، از اضافه بار سیستم و نشت حافظه جلوگیری کنید.
فشار برگشتی ژنراتورهای ناهمگام جاوااسکریپت: راهنمای نهایی برای کنترل جریان داده
در دنیای برنامههای کاربردی متمرکز بر داده، ما اغلب با یک مشکل کلاسیک مواجه میشویم: یک منبع داده سریع که اطلاعات را بسیار سریعتر از مصرفکننده تولید میکند. یک شلنگ آتش نشانی را تصور کنید که به یک آبپاش باغچه متصل است. بدون یک شیر برای کنترل جریان، یک آشفتگی سیلزده خواهید داشت. در نرمافزار، این سیل منجر به حافظه بیش از حد، برنامههای غیرپاسخگو و در نهایت خرابی میشود. این چالش اساسی توسط مفهومی به نام فشار برگشتی مدیریت میشود و جاوااسکریپت مدرن یک راهحل منحصراً ظریف ارائه میدهد: ژنراتورهای ناهمگام.
این راهنمای جامع شما را به یک غواصی عمیق در دنیای پردازش جریان و کنترل جریان در جاوااسکریپت خواهد برد. ما بررسی خواهیم کرد که فشار برگشتی چیست، چرا برای ساخت سیستمهای قوی حیاتی است و چگونه ژنراتورهای ناهمگام یک مکانیسم شهودی و داخلی برای رسیدگی به آن ارائه میدهند. چه در حال پردازش فایلهای بزرگ باشید، چه در حال مصرف APIهای بیدرنگ یا ساخت خطوط لوله داده پیچیده، درک این الگو اساساً نحوه نوشتن کد ناهمگام شما را تغییر میدهد.
1. از بین بردن مفاهیم اصلی
قبل از اینکه بتوانیم یک راهحل بسازیم، ابتدا باید قطعات اساسی پازل را درک کنیم. اجازه دهید اصطلاحات کلیدی را روشن کنیم: جریانها، فشار برگشتی و جادوی ژنراتورهای ناهمگام.
جریان چیست؟
جریان یک تکه داده نیست. این یک توالی از داده است که در طول زمان در دسترس قرار میگیرد. به جای خواندن یک فایل 10 گیگابایتی کامل به یکباره در حافظه (که احتمالاً برنامه شما را خراب میکند)، میتوانید آن را به صورت جریان، قطعه به قطعه، بخوانید. این مفهوم در محاسبات جهانی است:
- ورودی/خروجی فایل: خواندن یک فایل گزارش بزرگ یا نوشتن دادههای ویدئویی.
- شبکهسازی: دانلود یک فایل، دریافت داده از یک WebSocket، یا پخش محتوای ویدئویی.
- ارتباطات بین فرآیندی: انتقال خروجی یک برنامه به ورودی برنامه دیگر.
جریانها برای کارایی ضروری هستند و به ما امکان میدهند مقادیر زیادی از دادهها را با حداقل ردپای حافظه پردازش کنیم.
فشار برگشتی چیست؟
فشار برگشتی مقاومت یا نیرویی است که با جریان مورد نظر داده مخالفت میکند. این یک مکانیسم بازخورد است که به یک مصرفکننده کند اجازه میدهد به یک تولیدکننده سریع سیگنال دهد، «هی، سرعتت رو کم کن! من نمیتونم ادامه بدم.»
بیایید از یک تشبیه کلاسیک استفاده کنیم: یک خط مونتاژ کارخانه.
- تولیدکننده اولین ایستگاه است که قطعات را با سرعت بالا روی نوار نقاله قرار میدهد.
- مصرفکننده ایستگاه نهایی است که باید یک مونتاژ کند و دقیق را روی هر قطعه انجام دهد.
اگر تولیدکننده خیلی سریع باشد، قطعات جمع میشوند و در نهایت قبل از رسیدن به مصرفکننده از تسمه میافتند. این از دست رفتن دادهها و خرابی سیستم است. فشار برگشتی سیگنالی است که مصرفکننده به سمت بالا ارسال میکند و به تولیدکننده میگوید تا زمانی که به آن رسیده است مکث کند. این تضمین میکند که کل سیستم با سرعت کندترین جزء خود کار میکند و از اضافه بار جلوگیری میکند.
بدون فشار برگشتی، شما در معرض خطر:
- بافرینگ نامحدود: دادهها در حافظه جمع میشوند که منجر به استفاده زیاد از RAM و خرابی احتمالی میشود.
- از دست رفتن دادهها: اگر بافرها سرریز شوند، دادهها ممکن است حذف شوند.
- مسدود کردن حلقه رویداد: در Node.js، یک سیستم بیش از حد بارگذاری شده میتواند حلقه رویداد را مسدود کند و برنامه را غیرپاسخگو کند.
یک تازهکننده سریع: ژنراتورها و تکرارکنندههای ناهمگام
راه حل فشار برگشتی در جاوااسکریپت مدرن در ویژگیهایی نهفته است که به ما امکان میدهند اجرا را متوقف و از سر بگیریم. بیایید آنها را سریع بررسی کنیم.
ژنراتورها (`function*`): اینها توابعی هستند که میتوان از آنها خارج شد و بعداً دوباره وارد شد. آنها از کلیدواژه `yield` برای «مکث» و بازگرداندن یک مقدار استفاده میکنند. سپس فراخوان میتواند تصمیم بگیرد که چه زمانی اجرای تابع را برای دریافت مقدار بعدی از سر بگیرد. این یک سیستم مبتنی بر درخواست را برای دادههای همزمان ایجاد میکند.
تکرارکنندههای ناهمگام (`Symbol.asyncIterator`): این یک پروتکل است که نحوه تکرار بر روی منابع داده ناهمگام را تعریف میکند. یک شیء یک تکرارپذیر ناهمگام است اگر متدی با کلید `Symbol.asyncIterator` داشته باشد که یک شیء را با یک متد `next()` برمیگرداند. این متد `next()` یک Promise را برمیگرداند که به `{ value, done }` ختم میشود.
ژنراتورهای ناهمگام (`async function*`): این جایی است که همه چیز با هم جمع میشود. ژنراتورهای ناهمگام رفتار مکث ژنراتورها را با ماهیت ناهمگام Promises ترکیب میکنند. آنها ابزار کاملی برای نشان دادن جریانی از داده هستند که در طول زمان میرسد.
شما یک ژنراتور ناهمگام را با استفاده از حلقه قدرتمند `for await...of` مصرف میکنید، که پیچیدگی فراخوانی `.next()` و انتظار برای حل شدن promises را از بین میبرد.
async function* countToThree() {
yield 1; // Pause and yield 1
await new Promise(resolve => setTimeout(resolve, 1000)); // Asynchronously wait
yield 2; // Pause and yield 2
await new Promise(resolve => setTimeout(resolve, 1000));
yield 3; // Pause and yield 3
}
async function main() {
console.log("Starting consumption...");
for await (const number of countToThree()) {
console.log(number); // This will log 1, then 2 after 1s, then 3 after another 1s
}
console.log("Finished consumption.");
}
main();
نکته کلیدی این است که حلقه `for await...of` مقادیر را از ژنراتور میکشد. تا زمانی که کد داخل حلقه اجرای خود را برای مقدار فعلی تمام نکرده است، از مقدار بعدی درخواست نخواهد کرد. این ماهیت کشش محور ذاتی راز فشار برگشتی خودکار است.
2. مشکل نشان داده شده است: جریان بدون فشار برگشتی
برای اینکه واقعاً از راهحل قدردانی کنید، بیایید به یک الگوی رایج اما معیوب نگاه کنیم. تصور کنید که ما یک منبع داده بسیار سریع (یک تولیدکننده) و یک پردازنده داده کند (یک مصرفکننده) داریم، شاید کسی که در یک پایگاه داده کند مینویسد یا یک API با محدودیت نرخ را فراخوانی میکند.
در اینجا یک شبیهسازی با استفاده از یک رویکرد سنتی منتشرکننده رویداد یا سبک کالبک وجود دارد، که یک سیستم مبتنی بر فشار است.
// Represents a very fast data source
class FastProducer {
constructor() {
this.listeners = [];
}
onData(listener) {
this.listeners.push(listener);
}
start() {
let id = 0;
// Produce data every 10 milliseconds
this.interval = setInterval(() => {
const data = { id: id++, timestamp: Date.now() };
console.log(`PRODUCER: Emitting item ${data.id}`);
this.listeners.forEach(listener => listener(data));
}, 10);
}
stop() {
clearInterval(this.interval);
}
}
// Represents a slow consumer (e.g., writing to a slow network service)
async function slowConsumer(data) {
console.log(` CONSUMER: Starting to process item ${data.id}...`);
// Simulate a slow I/O operation taking 500 milliseconds
await new Promise(resolve => setTimeout(resolve, 500));
console.log(` CONSUMER: ...Finished processing item ${data.id}`);
}
// --- Let's run the simulation ---
const producer = new FastProducer();
const dataBuffer = [];
producer.onData(data => {
console.log(`Received item ${data.id}, adding to buffer.`);
dataBuffer.push(data);
// A naive attempt to process
// slowConsumer(data); // This would block new events if we awaited it
});
producer.start();
// Let's inspect the buffer after a short time
setTimeout(() => {
producer.stop();
console.log(`
--- After 2 seconds ---`);
console.log(`Buffer size is: ${dataBuffer.length}`);
console.log(`Producer created around 200 items, but the consumer would have only processed 4.`);
console.log(`The other 196 items are sitting in memory, waiting.`);
}, 2000);
چه اتفاقی در اینجا میافتد؟
تولیدکننده هر 10 میلیثانیه دادهها را منتشر میکند. مصرفکننده 500 میلیثانیه طول میکشد تا یک مورد واحد را پردازش کند. تولیدکننده 50 برابر سریعتر از مصرفکننده است!
در این مدل مبتنی بر فشار، تولیدکننده کاملاً از وضعیت مصرفکننده بیاطلاع است. او فقط به فشار دادن دادهها ادامه میدهد. کد ما به سادگی دادههای ورودی را به یک آرایه، `dataBuffer` اضافه میکند. در عرض 2 ثانیه، این بافر تقریباً شامل 200 مورد است. در یک برنامه کاربردی واقعی که ساعتها اجرا میشود، این بافر بینهایت رشد میکند، تمام حافظه موجود را مصرف میکند و باعث خرابی فرآیند میشود. این مشکل فشار برگشتی در خطرناکترین شکل آن است.
3. راهحل: فشار برگشتی ذاتی با ژنراتورهای ناهمگام
اکنون، بیایید همان سناریو را با استفاده از یک ژنراتور ناهمگام بازسازی کنیم. ما تولیدکننده را از یک «فشاردهنده» به چیزی که میتوان از آن «کشید» تبدیل خواهیم کرد.
ایده اصلی این است که منبع داده را در یک `async function*` قرار دهیم. سپس مصرفکننده از یک حلقه `for await...of` برای کشیدن دادهها فقط زمانی که آماده دریافت اطلاعات بیشتر است استفاده خواهد کرد.
// PRODUCER: A data source wrapped in an async generator
async function* createFastProducer() {
let id = 0;
while (true) {
// Simulate a fast data source creating an item
await new Promise(resolve => setTimeout(resolve, 10));
const data = { id: id++, timestamp: Date.now() };
console.log(`PRODUCER: Yielding item ${data.id}`);
yield data; // Pause until the consumer requests the next item
}
}
// CONSUMER: A slow process, just like before
async function slowConsumer(data) {
console.log(` CONSUMER: Starting to process item ${data.id}...`);
// Simulate a slow I/O operation taking 500 milliseconds
await new Promise(resolve => setTimeout(resolve, 500));
console.log(` CONSUMER: ...Finished processing item ${data.id}`);
}
// --- The main execution logic ---
async function main() {
const producer = createFastProducer();
// The magic of `for await...of`
for await (const data of producer) {
await slowConsumer(data);
}
}
main();
بیایید جریان اجرا را تجزیه و تحلیل کنیم
اگر این کد را اجرا کنید، خروجی بسیار متفاوتی را مشاهده خواهید کرد. چیزی شبیه این خواهد بود:
PRODUCER: Yielding item 0 CONSUMER: Starting to process item 0... CONSUMER: ...Finished processing item 0 PRODUCER: Yielding item 1 CONSUMER: Starting to process item 1... CONSUMER: ...Finished processing item 1 PRODUCER: Yielding item 2 CONSUMER: Starting to process item 2... ...
به همگامسازی کامل توجه کنید. تولیدکننده فقط یک مورد جدید را بعد از این که مصرفکننده پردازش قبلی را کاملاً به پایان رساند، بازده میدهد. هیچ بافر رو به رشدی وجود ندارد و هیچ نشت حافظهای وجود ندارد. فشار برگشتی به طور خودکار به دست میآید.
در اینجا تجزیه گام به گام از دلیل عملکرد این روش آمده است:
- حلقه `for await...of` شروع میشود و پشت صحنه `producer.next()` را فراخوانی میکند تا اولین مورد را درخواست کند.
- تابع `createFastProducer` اجرا را شروع میکند. 10 میلیثانیه صبر میکند، `data` را برای آیتم 0 ایجاد میکند و سپس به `yield data` میرسد.
- ژنراتور اجرای خود را متوقف میکند و یک Promise را برمیگرداند که با مقدار حاصلشده (`{ value: data, done: false }`) حل میشود.
- حلقه `for await...of` مقدار را دریافت میکند. بدنه حلقه شروع به اجرا با این آیتم داده اول میکند.
- این `await slowConsumer(data)` را فراخوانی میکند. این 500 میلیثانیه طول میکشد تا کامل شود.
- این مهمترین قسمت است: حلقه `for await...of` دوباره `producer.next()` را فراخوانی نمیکند تا زمانی که Promise `await slowConsumer(data)` حل شود. تولیدکننده در عبارت `yield` خود متوقف میماند.
- پس از 500 میلیثانیه، `slowConsumer` تمام میشود. بدنه حلقه برای این تکرار کامل است.
- اکنون، و تنها در حال حاضر، حلقه `for await...of` دوباره `producer.next()` را فراخوانی میکند تا مورد بعدی را درخواست کند.
- تابع `createFastProducer` از جایی که متوقف شده است خارج میشود و حلقه `while` خود را ادامه میدهد، چرخه را برای مورد 1 شروع میکند.
سرعت پردازش مصرفکننده مستقیماً سرعت تولید تولیدکننده را کنترل میکند. این یک سیستم مبتنی بر کشش است و این پایه کنترل جریان ظریف در جاوااسکریپت مدرن است.
4. الگوهای پیشرفته و موارد استفاده در دنیای واقعی
قدرت واقعی ژنراتورهای ناهمگام زمانی آشکار میشود که شروع به ترکیب آنها در خطوط لوله برای انجام تبدیلهای دادهای پیچیده میکنید.
تزریق و تبدیل جریانها
همانطور که میتوانید دستورات را در یک خط فرمان یونیکس (به عنوان مثال، `cat log.txt | grep 'ERROR' | wc -l`) لوله کنید، میتوانید ژنراتورهای ناهمگام را نیز زنجیر کنید. یک ترانسفورمر به سادگی یک ژنراتور ناهمگام است که یک تکرارپذیر ناهمگام دیگر را به عنوان ورودی خود میپذیرد و دادههای تبدیلشده را تولید میکند.
بیایید تصور کنیم که در حال پردازش یک فایل CSV بزرگ از دادههای فروش هستیم. ما میخواهیم فایل را بخوانیم، هر خط را تجزیه کنیم، برای معاملات با ارزش بالا فیلتر کنیم و سپس آنها را در یک پایگاه داده ذخیره کنیم.
const fs = require('fs');
const { once } = require('events');
// PRODUCER: Reads a large file line by line
async function* readFileLines(filePath) {
const readable = fs.createReadStream(filePath, { encoding: 'utf8' });
let buffer = '';
readable.on('data', chunk => {
buffer += chunk;
let newlineIndex;
while ((newlineIndex = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, newlineIndex);
buffer = buffer.slice(newlineIndex + 1);
readable.pause(); // Explicitly pause Node.js stream for backpressure
yield line;
}
});
readable.on('end', () => {
if (buffer.length > 0) {
yield buffer; // Yield the last line if no trailing newline
}
});
// A simplified way to wait for the stream to finish or error
await once(readable, 'close');
}
// TRANSFORMER 1: Parses CSV lines into objects
async function* parseCSV(lines) {
for await (const line of lines) {
const [id, product, amount] = line.split(',');
if (id && product && amount) {
yield { id, product, amount: parseFloat(amount) };
}
}
}
// TRANSFORMER 2: Filters for high-value transactions
async function* filterHighValue(transactions, minValue) {
for await (const tx of transactions) {
if (tx.amount >= minValue) {
yield tx;
}
}
}
// CONSUMER: Saves the final data to a slow database
async function saveToDatabase(transaction) {
console.log(`Saving transaction ${transaction.id} with amount ${transaction.amount} to DB...`);
await new Promise(resolve => setTimeout(resolve, 100)); // Simulate slow DB write
}
// --- The Composed Pipeline ---
async function processSalesFile(filePath) {
const lines = readFileLines(filePath);
const transactions = parseCSV(lines);
const highValueTxs = filterHighValue(transactions, 1000);
console.log("Starting ETL pipeline...");
for await (const tx of highValueTxs) {
await saveToDatabase(tx);
}
console.log("Pipeline finished.");
}
// Create a dummy large CSV file for testing
// fs.writeFileSync('sales.csv', ...);
// processSalesFile('sales.csv');
در این مثال، فشار برگشتی تمام مسیر را در امتداد زنجیره گسترش میدهد. `saveToDatabase` کندترین قسمت است. `await` آن باعث میشود که حلقه `for await...of` متوقف شود. این `filterHighValue` را متوقف میکند، که متوقف میشود از `parseCSV` برای دریافت موارد درخواست میکند، که متوقف میشود از `readFileLines` برای دریافت موارد درخواست میکند، که در نهایت به جریان فایل Node.js میگوید که به صورت فیزیکی خواندن از دیسک را `pause()` کند. کل سیستم با گام قفلشونده حرکت میکند، از حداقل حافظه استفاده میکند، و همه اینها توسط مکانیک ساده کشش تکرار ناهمگام تنظیم میشود.
مدیریت خطاهای ظریف
مدیریت خطا ساده است. شما میتوانید حلقه مصرفکننده خود را در یک بلوک `try...catch` قرار دهید. اگر خطایی در هر یک از ژنراتورهای بالادستی پرتاب شود، به سمت پایین منتشر میشود و توسط مصرفکننده گرفته میشود.
async function* errorProneGenerator() {
yield 1;
yield 2;
throw new Error("Something went wrong in the generator!");
yield 3; // This will never be reached
}
async function main() {
try {
for await (const value of errorProneGenerator()) {
console.log("Received:", value);
}
} catch (err) {
console.error("Caught an error:", err.message);
}
}
main();
// Output:
// Received: 1
// Received: 2
// Caught an error: Something went wrong in the generator!
پاکسازی منبع با `try...finally`
اگر مصرفکننده تصمیم بگیرد که پردازش را زود متوقف کند (به عنوان مثال، با استفاده از عبارت `break`) چه؟ ممکن است ژنراتور منابع باز مانند دستههای فایل یا اتصالات پایگاه داده را باز نگه دارد. بلوک `finally` در داخل یک ژنراتور مکان مناسبی برای پاکسازی است.
هنگامی که یک حلقه `for await...of` زودتر از موعد (از طریق `break`، `return` یا یک خطا) خارج میشود، به طور خودکار متد `.return()` ژنراتور را فراخوانی میکند. این باعث میشود که ژنراتور به بلوک `finally` خود برود و به شما امکان میدهد اقدامات پاکسازی را انجام دهید.
async function* fileReaderWithCleanup(filePath) {
let fileHandle;
try {
console.log("GENERATOR: Opening file...");
fileHandle = await fs.promises.open(filePath, 'r');
// ... logic to yield lines from the file ...
yield 'line 1';
yield 'line 2';
yield 'line 3';
} finally {
if (fileHandle) {
console.log("GENERATOR: Closing file handle.");
await fileHandle.close();
}
}
}
async function main() {
for await (const line of fileReaderWithCleanup('my-file.txt')) {
console.log("CONSUMER:", line);
if (line === 'line 2') {
console.log("CONSUMER: Breaking the loop early.");
break; // Exit the loop
}
}
}
main();
// Output:
// GENERATOR: Opening file...
// CONSUMER: line 1
// CONSUMER: line 2
// CONSUMER: Breaking the loop early.
// GENERATOR: Closing file handle.
5. مقایسه با سایر مکانیسمهای فشار برگشتی
ژنراتورهای ناهمگام تنها راه برای رسیدگی به فشار برگشتی در اکوسیستم جاوااسکریپت نیستند. درک این که چگونه آنها با رویکردهای محبوب دیگر مقایسه میشوند، مفید است.
جریانهای Node.js (`.pipe()` و `pipeline`)
Node.js یک API Streams داخلی قدرتمند دارد که سالهاست فشار برگشتی را مدیریت میکند. هنگامی که از `.pipe(writable)` استفاده میکنید، Node.js جریان داده را بر اساس بافرهای داخلی و تنظیمات `highWaterMark` مدیریت میکند. این یک سیستم رویدادمحور، مبتنی بر فشار با مکانیسمهای فشار برگشتی داخلی است.
- پیچیدگی: پیادهسازی صحیح API Streams Node.js به ویژه برای جریانهای تبدیل سفارشی، به طرز رسوایی پیچیده است. شامل توسعه کلاسها و مدیریت وضعیت داخلی و رویدادها (`'data'`، `'end'`، `'drain'`) است.
- مدیریت خطا: مدیریت خطا با `.pipe()` دشوار است، زیرا خطا در یک جریان به طور خودکار جریانهای دیگر را در خط لوله از بین نمیبرد. به همین دلیل بود که `stream.pipeline` به عنوان یک جایگزین قویتر معرفی شد.
- خوانایی: ژنراتورهای ناهمگام اغلب به کدی منجر میشوند که همزمانتر به نظر میرسد و مسلماً خواندن و استدلال در مورد آن، بهویژه برای تبدیلهای پیچیده، آسانتر است.
برای I/O با عملکرد بالا و سطح پایین در Node.js، API Streams بومی هنوز یک انتخاب عالی است. با این حال، برای منطق سطح برنامه و تبدیل دادهها، ژنراتورهای ناهمگام اغلب تجربه توسعهدهنده سادهتر و ظریفتری را ارائه میدهند.
برنامهنویسی واکنشی (RxJS)
کتابخانههایی مانند RxJS از مفهوم Observables استفاده میکنند. Observables مانند جریانهای Node.js، در درجه اول یک سیستم مبتنی بر فشار هستند. یک تولیدکننده (Observable) مقادیر را منتشر میکند و یک مصرفکننده (Observer) به آنها واکنش نشان میدهد. فشار برگشتی در RxJS خودکار نیست. باید به طور صریح با استفاده از انواع اپراتورها مانند `buffer`، `throttle`، `debounce` یا برنامهریزهای سفارشی مدیریت شود.
- الگو: RxJS یک الگوی برنامهنویسی تابعی قدرتمند برای ترکیب و مدیریت جریانهای رویداد ناهمگام پیچیده ارائه میدهد. برای سناریوهایی مانند مدیریت رویدادهای UI بسیار قدرتمند است.
- منحنی یادگیری: RxJS به دلیل تعداد زیادی اپراتور و تغییر در تفکر مورد نیاز برای برنامهنویسی واکنشی، یک منحنی یادگیری شیبدار دارد.
- کشش در مقابل فشار: تفاوت کلیدی باقی میماند. ژنراتورهای ناهمگام اساساً مبتنی بر کشش هستند (مصرفکننده کنترل را در دست دارد)، در حالی که Observables مبتنی بر فشار هستند (تولیدکننده کنترل را در دست دارد و مصرفکننده باید به فشار واکنش نشان دهد).
ژنراتورهای ناهمگام یک ویژگی زبان بومی هستند، که آنها را به یک انتخاب سبک و بدون وابستگی برای بسیاری از مشکلات فشار برگشتی تبدیل میکند که در غیر این صورت ممکن است به یک کتابخانه جامع مانند RxJS نیاز داشته باشند.
نتیجهگیری: کشش را در آغوش بگیرید
فشار برگشتی یک ویژگی اختیاری نیست. این یک نیاز اساسی برای ساخت برنامههای پردازش داده پایدار، مقیاسپذیر و با حافظه کارآمد است. غفلت از آن دستوری برای خرابی سیستم است.
سالهاست که توسعهدهندگان جاوااسکریپت برای مدیریت کنترل جریان به APIهای مبتنی بر رویداد پیچیده یا کتابخانههای شخص ثالث متکی بودهاند. با معرفی ژنراتورهای ناهمگام و نحو `for await...of`، اکنون یک ابزار قدرتمند، بومی و شهودی داریم که مستقیماً در زبان تعبیه شده است.
با تغییر از یک مدل مبتنی بر فشار به یک مدل مبتنی بر کشش، ژنراتورهای ناهمگام فشار برگشتی ذاتی را ارائه میدهند. سرعت پردازش مصرفکننده به طور طبیعی سرعت تولیدکننده را دیکته میکند و منجر به کدی میشود که:
- ایمن از حافظه: بافرهای نامحدود را حذف میکند و از خرابیهای خارج از حافظه جلوگیری میکند.
- قابل خواندن: منطق ناهمگام پیچیده را به حلقههای ساده و به ظاهر متوالی تبدیل میکند.
- قابل ترکیب: امکان ایجاد خطوط لوله تبدیل دادههای ظریف و قابل استفاده مجدد را فراهم میکند.
- مقاوم: مدیریت خطا و مدیریت منابع را با بلوکهای استاندارد `try...catch...finally` ساده میکند.
دفعه بعد که نیاز به پردازش جریانی از دادهها دارید، خواه از یک فایل، یک API یا هر منبع ناهمگام دیگری باشد، به بافر دستی یا کالبکهای پیچیده نروید. ظرافت مبتنی بر کشش ژنراتورهای ناهمگام را در آغوش بگیرید. این یک الگوی جاوااسکریپت مدرن است که کد ناهمگام شما را تمیزتر، ایمنتر و قدرتمندتر میکند.